<?php
/*'****************************************************************************\
    CHTML 0.2.1
 	Copyright (C) 2007, 2008 Alexey Ilyin

 	This library is free software; you can redistribute it and/or modify it
	under the terms of the GNU Lesser General Public License as published
	by the Free Software Foundation; either version 2.1 of the License, or
	(at your option) any later version.

 	This library is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	Lesser General Public License for more details.

 	You should have received a copy of the GNU Lesser General Public
	License along with this library; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.

\******************************************************************************/

require_once 'chtml.inc.php';

class CFormProcessor extends CHTMLTemplate {
    protected $subname = 'submit';
    protected $refname = 'ref';
    
	/*function CFormProcessor ($tpl = NULL, $charset = NULL) {
        $this->CHTMLTemplate($tpl, $charset);
	}*/

    function formClass () {
        //  CAN BE OVERRIDDEN  //
        return 'CForm';
    }
    /**
     *  Loads or find mode template and returns cform instance.
     *  It uses form[@id='$mode'] XPath by default.
     */
    function form ($mode, $selector = '@id') {
        return $this->element("~*[$selector='$mode']", 0);
    }
    /**
     *  $mode - default mode (if no mode submitted).
     *  $referer - a url for cancelling the form.
     */
    function process ($mode, $referer = NULL) {
        //  Directly gets 'next' mode possibly submitted
        //  ('cause there ever is no form at this point).
        return $this->force(getdef($this->request(
            $this->subname), $mode), $referer);
    }

    function force ($mode, $referer = NULL) {
        if (is_array($mode)) {
            list($mode) = each($mode);
        }
        switch ($mode) {
        case 'close':
            //  Predefined method.
            $form = NULL;
            break;
        default:
            //  Prepare user-defined processing.
            //  First, find or load the form.
            $form = $this->form($mode);
            //  Pre-setup the referer for close method.
            if ($form && setdef($referer, $_SERVER['HTTP_REFERER'])) {
                $form->append('input', array(
                    'type'  => 'hidden',
                    'name'  => $this->refname,
                    'value' => $referer
                ));
            }
            break;
        }
        //  Do process.
        return $this->handle($mode, $form);
    }
    protected function handle ($mode, $form) {
        if (!method_exists($this, $mode)) FATAL_ERROR (
            "Mode $mode is not supported"
        );
        //  Do process.
        return $this->$mode($form);
    }
    /**
     *  @b Statically calls the request method of $this->formClass.
     *  Use it if there no real form provided, e.g. when you
     *  just process submitted values.
     */
    protected function request ($name = NULL) {
        return call_user_func_array(array($this->formClass(),
            'request'), $name);
    }
    /**
     *  Predefined mode close.
     */
    function close () {
        exit_to_location(self::unmix($this->request($this->refname)));
    }

    //////  DB  ////////////////////////////////////////////////////////////////

    function setDB ($db) {
        $this->db = $db;
    }

    function fetchDB ($sql, $callback, $arg = NULL) {
        if (!$this->db) FATAL_ERROR (
            "DB support is not set up"
        );
        $this->db->walk($sql, $callback, $arg);
    }

}

/////// ELEMENTS ///////////////////////////////////////////////////////////////

abstract class CFormMarkup extends CElement {
    /*function CFormMarkup ($base, $node) {
        $this->CMarkupElement($base, $node);
    }*/
    /**
     *  Parses name[index] (a special PHP element name form).
     *  Returns array(name => index | null).
     */
    protected static function parseName ($name) {
        assert('isset($name)');
        preg_match('/^([^\[]+)(\[(.*)\])?$/', $name, $ex);
        return array($ex[1] => $ex[2] ? $ex[3] : NULL);
    }

    protected static function makeName ($name, $index) {
        if (isset($index)) {
            /*if (is_int(index))
                $index = '';*/
            $index = "[$index]";
        }
        return $name.$index;
    }

    //////  DB  ////////////////////////////////////////////////////////////////

    /*function fetchDB ($sql, $callback, $arg = NULL) {
        $this->base->fetchDB($sql, $callback, $arg);
    }*/
}

//////  FORM  //////////////////////////////////////////////////////////////////

class CForm extends CFormMarkup {
    /*function CForm ($base, $node) {
        $this->CFormMarkup($base, $node);
    }*/

    static function create($base, $node) {
        $class = $base->formClass();
        return new $class($base, $node);
    }
    /**
     *  \b Statically takes all values from request
     *  source ($_REQUEST by default).
     *  Its main mission is to provide a possibility
     *  to override request source on per-(form)class basis.
     */
    static function request ($name = NULL) {
        //  CAN BE OVERRIDDEN  //
        return isset($name) ? $_REQUEST[$name] : $_REQUEST;
    }

    function hideRequest () {
        $result = array();
        foreach ($this->request() as $name => $value) {
            $result[$name] = $this->hideValue($name, $value);
        }
        return $result;
    }

    function hideValue ($name, $value = NULL) {
        isset($value) or $value = $this->request($name);
        if (is_array($value)) {
            $result = array();
            foreach ($value as $idx => $val) {
                $result[$idx] = $this->hideValue(
                    $this->makeName($name, $idx), $val);
            }
            return $result;
        }
        $this->append('input', array(
            'type'  => 'hidden',
            'name'  => $name,
            'value' => $value
        ));
        return $value;
    }

    function setAllValues () {
        foreach ($this->allNames() as $name => $indices) {
            $value = $this->request($name);
            if (!isset($value)) continue;
            if ($indices) foreach ($indices as $idx) {
                self::unmix($this->element($this->makeName($name, $idx),
                    'setDefValue', $idx ? $value[$idx] : $value));
            }
            else {
                self::unmix($this->element($name,
                    'setDefValue', $value));
            }
        }
        /*$names = $this->allNames();
        foreach ($this->request() as $name => $value) {
            $indices = $names[$name];
            if ($indices) foreach ($indices as $idx) {
                self::unmix($this->element($this->makeName($name, $idx),
                    'setDefValue', $idx ? $value[$idx] : $value));
            }
            else {
                self::unmix($this->element($name,
                    'setDefValue', $value));
            }
        }*/
    }
    /**
     *  Returns values for all form \b controls taken from the
     *  requested  source.
     *  @see CForm::request, CFormProcessor::request.
     */
    function allValues ($selector = NULL) {
        $result = array();
        $container = $selector ?
            $this->element($selector,0) : $this;
        foreach ($this->allNames($selector) as $name => $indices) {
            if ($indices) foreach ($indices as $idx) {
                $result[$name] = self::unmix($container->element(
                    $this->makeName($name, $idx), 'getValue'));
            }
            else {
                $result[$name] = self::unmix($container->element($name,
                    'getValue'));
            }
        }
        return $result;
    }
    /**
     *  array(
     *      name => array(sub, ...) | null,
     *      ...
     *  )
     */
    function allNames ($selector = NULL) {
        $result = array();
        $container = $selector ?
            $this->element($selector,0) : $this;
        $nodes = $container->query('.//@name');
        foreach ($nodes as $node) {
            $name = $this->parseName($node->nodeValue);
            list($name, $idx) = each($name);
            $entry = &$result[$name];
            if (isset($idx) && !@in_array($idx, $entry)) {
                $entry[] = $idx;
            }
        }
        return $result;
    }

    function source ($setAllValues = TRUE) {
        if ($setAllValues) try {
            $this->setAllValues();
        }
        catch (Exception $e) {
            trigger_error(__METHOD__.' failed: '
                .$e->getmessage());
        }
        return parent::source();
    }
    
    //////  UPLOADS  ///////////////////////////////////////////////////////////

    protected $uploaddir = './files/';

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*\
        $_FILES['userfile']['name']
        The original name of the file on the client machine.

        $_FILES['userfile']['type']
        The mime type of the file, if the browser provided this information.
        An example would be "image/gif". This mime type is however not
        checked on the PHP side and therefore don't take its value for
        granted.

        $_FILES['userfile']['size']
        The size, in bytes, of the uploaded file.

        $_FILES['userfile']['tmp_name']
        The temporary filename of the file in which the uploaded file was
        stored on the server.

        $_FILES['userfile']['error']
        The error code associated with this file upload.
    \*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

    function getUpload ($elem, $file) {
        if (!$file || !$file['name']) {
            return ''; // NULL makes not-null insertions to fail;
        }
        if ($file['error'] != UPLOAD_ERR_OK) throw new Exception (
            "Upload error: ".join(',', $file)
        );
        $uploadfile = $this->whereToStoreUpload($elem, $file);
        if (!move_uploaded_file($file['tmp_name'], $uploadfile))
            throw new Exception ('Possible file upload attack!');
        $elem->setDefValue($uploadfile);
        return $uploadfile;
    }

    protected function whereToStoreUpload ($elem, $file) {
        $src = pathinfo($file['name']);
        return $this->uploaddir
            .basename($file['tmp_name'], '.tmp')
            .".$src[extension]";
    }

    function setUpload($elem, $value) {
        // TO BE OVERRIDDEN //
    }
}

//////  FORM ELEMENTS  /////////////////////////////////////////////////////////

abstract class CFormElement extends CFormMarkup {
    var $forceDefault;
    
    /*function CFormElement ($base, $node) {
        $this->CFormMarkup($base, $node);
    }*/

    /*function get ($aName) {
        $getm = 'get'.$aName;
        return method_exists($this, $getm) ?
             $this->$getm() : $this[$aName];
    }*/

    function getName () {
        return key($this->parseName($this['name'])); //???//
    }
    /**
     *  Unconditionally gets a value by parameter name
     *  from the user request.
     */
    function request () {
        return $this->base->request($this->getName());
    }
    /**
     *  Gets value from user request or predefined value
     *  depending on the forceDefault.
     */
    function getValue () {
        return $this->forceDefault ? $this->getDefValue() :
            getdef($this->request(), $this->getDefValue());
    }

    protected function getDefValue () {
        return $this['value'];
    }
    
    /** Alias of setDefValue */
    function setValue ($value) {
        return $this->setDefValue($value);
    }
    
    function setDefValue ($value) {
        if (is_array($value)) {
            $name = $this->parseName($this['name']);
            list($name, $index) = each($name);
            if (!isset($index)) throw new Exception (
                __METHOD__." '$name' failed to accept an array"
            );
            $value = $value[$index];
        }
        return $this['value'] = $value;
    }

    protected function check ($value, $checked) {
        $test = $this['value'];
        /** @todo handle a case of the same values
         *  for different keys correctly */
        $test = is_array($value) ?
            in_array($test, $value) : ($test == $value);
        if ($test)
            $this[$checked] = TRUE;
        else
            unset($this[$checked]);
    }
}

class CInput extends CFormElement {
    /*function CInput ($base, $node) {
        $this->CFormElement($base, $node);
    }*/
    static function create($base, $node) {
        $class = $node->getAttribute('instance-of');
        if (!$class) {
            $class = $node->getAttribute('type');
            $class = 'c'.($class ? $class : 'text');
        }
        $elem = new $class($base, $node);
        return $elem;
    }
}

class CText extends CInput {}

class CPassword extends CInput {}

class CHidden extends CInput {}

/**
 *  Mostly intended to force a query
 *  parameter to be an array.
 */
class CDead extends CHidden {
    function setDefValue () {}
}

class CSubmit extends CInput {
    function setDefValue (/*$value*/) {}
}

class CCheckbox extends CInput {
    /*function CCheckbox($base, $node) {
        $this->CInput($base, $node);
    }*/
    function setDefValue ($value) {
        $this->check($value, 'checked');
    }
}

class CRadio extends CInput {
    /*function CRadio($base, $node) {
        $this->CInput($base, $node);
    }*/
    function setDefValue ($value) {
        $this->check($value, 'checked');
    }
}

abstract class CFormElementDV extends CFormElement {
    /*protected function getDefValue () {
    }
    function setDefValue ($value) {
    }*/
}

class CTextarea extends CFormElementDV {
    static function create($base, $node) {
        return new CTextarea($base, $node);
    }
    protected function getDefValue () {
        return $this->nodeValue;
    }
    function setDefValue ($value) {
        return $this->nodeValue = $value;
    }
}

class CButton extends CFormElementDV {
    static function create($base, $node) {
        return new CButton($base, $node);
    }
    protected function getDefValue () {
        return $this->nodeValue;
    }
    function setDefValue ($value) {
        return $this->nodeValue = $value;
    }
}

class CSelect extends CFormElementDV {
    static function create($base, $node) {
        return new CSelect($base, $node);
    }
    function addOptions ($options) {
        foreach ($options as $value => $label) {
            $elem = $this->append('option', array(
                'value' => $value
            ), $label);
            $this->cached['option'][] = $elem;
        }
    }
    protected function getDefValue () {
        try {
            return self::unmix($this->element(
                '~option[@selected]', 'getValue'));
        }
        catch (Exception $e) {
            return NULL;
        }
    }
    function setDefValue ($value) {
        $this->element('~option', 'setDefValue', $value);
    }
}

class COption extends CFormElement {
    static function create($base, $node) {
        return new COption($base, $node);
    }
    function setDefValue ($value) {
        $this->check($value, 'selected');
    }
}

class CFieldSet extends CFormMarkup {
    static function create($base, $node) {
        return new CFieldSet($base, $node);
    }
}

class CFile extends CFormElementDV {
    protected function getDefValue () {
        return $this->base->getUpload($this, $_FILES[$this->getName()]);
    }

    function setDefValue ($value) {
        return $this->base->setUpload($this, $value);
    }
}

?>